"""
Data Manager module.

This module handle locks to control data acess.

Last Modified: $Id: datamanager.py 31 2005-05-31 20:02:02Z gustavo $

@author: U{Gustavo Sverzut Barbieri <barbieri@gmail.com>}
@license: GNU GPL
@copyright: (C) 2005 Gustavo Sverzut Barbieri
@version: $Rev: 31 $
"""









import threading
from synchronized import Synchronized
from set import Set
from debug import debug

import time
import sys


class DataManagerDeadlock( Exception ):
    """Data Manager Dead-Lock Exception.

    This exception is raised if a lock cannot be acquired or if a deadlock
    was detected.
    """
    XLOCK = 1
    SLOCK = 2

    locktype_map = { XLOCK: "exclusive",
		     SLOCK: "shared" }

    def __init__( self, locktype, tid, id ):
	self.locktype = locktype
	self.tid      = tid
	self.id       = id
    # __init__()


    def __str__( self ):
	return "%s: %s lock acquisition aborted due timeout. "\
	       "ID: %s, TID: %s" % \
	       ( self.__class__.__name__, self.locktype_map[ self.locktype ],
		 self.id, self.tid )
    # __str__()
# DataManagerDeadlock()



class DataManager( Synchronized ):
    """Data Manager: handle access to data

    Creating a Data Manager (you must use the singleton!):

    >>> dm = singleton()

    Acquiring Shared Locks:

    >>> dm.slock( 1, 1 )
    True
    >>> dm.slock( 2, 1 )
    True
    >>> dm.unlock( 2, 1 )
    True
    >>> dm.unlock( 1, 1 )
    True


    Acquiring Exclusive Locks:

    >>> dm.xlock( 1, 1 )
    True
    >>> dm.unlock( 1, 1 )
    True


    You can acquire Exclusive Locks more than once:

    >>> dm.xlock( 1, 1 )
    True
    >>> dm.xlock( 1, 1 )
    True
    >>> dm.xlock( 1, 1 )
    True
    >>> dm.unlock( 1, 1 )
    True
    >>> dm.is_locked( 1 )
    False


    You can't get exclusive locks for more than one transaction:

    >>> dm.xlock( 1, 1 )
    True
    >>> dm.xlock( 2, 1 )
    Traceback (most recent call last):
        ...
    DataManagerDeadlock: DataManagerDeadlock: exclusive lock acquisition aborted due timeout. ID: 1, TID: 2


    Shared locks are elevated to Exclusive Locks:

    >>> dm.slock( 1, 1 )
    True
    >>> dm.xlock( 1, 1 )
    True


    Dead locks are prevented through timeout:

    >>> dm.slock( 1, 1 )
    True
    >>> dm.xlock( 2, 1 )
    Traceback (most recent call last):
        ...
    DataManagerDeadlock: DataManagerDeadlock: exclusive lock acquisition aborted due timeout. ID: 1, TID: 2
    """
    timeout = 1
    retry_delay = timeout * 0.001

    def __init__( self ):
	Synchronized.__init__( self )
	self.__xlock = {}
	self.__slock = {}

	# reverse mappings
	self.__r_xlock = {}
	self.__r_slock = {}
    # __init__()


    def sync_xlock( self, tid, id ):
        """Acquire exclusive (write) lock.

        @param tid: transaction identifier.
        @param id: data identifier.

        @return: True if the lock was acquired, False otherwise.
        @rtype: L{bool}

        @raise DataManagerDeadlock: if lock cannot be acquired.
        """
	if self.is_xlocked_tid( tid, id ):
	    return True

	t     = time.time
	start = t()
        sleep = time.sleep

	while t() - start < self.timeout:
	    debug( 3, "Wait for xlock. TID: %s, id: %s" % ( tid, id ) )

	    if self.is_slocked_just_tid( tid, id ) or \
		   self.is_unlocked( id ):
		self.__do_xlock( tid, id )
		return True


	    else:
                sleep( self.retry_delay )
                if debug.level > 1:
		    try:
			debug( 3, "   it's still slocked to: %s" % \
                               self.__slock[ id ] )
		    except: pass
		    try:
			debug( 3, "   it's still xlocked to: %s " % \
                               self.__xlock[ id ] )
		    except: pass


	debug( 2, "xlock: Aborted... TID: %s, id: %s" % ( tid, id ) )

	raise DataManagerDeadlock( DataManagerDeadlock.XLOCK, tid, id )
    # xlock()


    def sync_slock( self, tid, id ):
        """Acquire shared (read) lock.

        @param tid: transaction identifier.
        @param id: data identifier.

        @return: True if the lock was acquired, False otherwise.
        @rtype: L{bool}

        @raise DataManagerDeadlock: if lock cannot be acquired.
        """
	if self.is_slocked_tid( tid, id ) or \
	   self.is_xlocked_tid( tid, id ):
	    return True

	t     = time.time
	start = t()
        sleep = time.sleep

	while t() - start < self.timeout:

	    debug( 3, "Wait for slock. TID: %s, id: %s" % ( tid, id ) )

	    if not self.is_xlocked( id ):
		self.__do_slock( tid, id )
		return True
	    else:
                sleep( self.retry_delay )
		debug( 3, "   it's still xlocked to %s" % \
                       self.__xlock[ id ] )

        debug( 2, "slock: Aborted... TID: %s, id: %s" % ( tid, id ) )

	raise DataManagerDeadlock( DataManagerDeadlock.SLOCK, tid, id )
    # slock()


    def sync_is_xlocked( self, id ):
        """Check if data is exclusive locked.

        @param id: data identifier to check

        @return: True if it's locked, False otherwise
        @rtype: L{bool}
        """
	return id in self.__xlock
    # is_xlocked()


    def sync_is_slocked( self, id ):
        """Check if data is shared locked.

        @param id: data identifier to check

        @return: True if it's locked, False otherwise
        @rtype: L{bool}
        """
	return id in self.__slock
    # is_slocked()


    def sync_is_unlocked( self, id ):
        """Check if data is unlocked.

        @param id: data identifier to check

        @return: True if it's not locked, False otherwise.
        @rtype: L{bool}
        """
	return not self.is_locked( id )
    # is_unlocked()


    def sync_is_locked( self, id ):
        """Check if data is locked.

        @param id: data identifier to check

        @return: True if it's locked, False otherwise.
        @rtype: L{bool}
        """
	return self.is_slocked( id ) # xlock also slocks
    # is_locked()


    def sync_is_xlocked_tid( self, tid, id ):
        """Check if data C{id} is exclusive locked to C{tid}.

        @param tid: transaction identifier to check.
        @param id: data identifier to check.

        @return: True if it's locked to C{tid}, False otherwise.
        @rtype: L{bool}
        """
	try:
	    return self.__xlock[ id ] == tid
	except KeyError:
	    return False
    # is_xlocked_tid()


    def sync_is_slocked_tid( self, tid, id ):
        """Check if data C{id} is shared locked to C{tid}.

        @param tid: transaction identifier to check.
        @param id: data identifier to check.

        @return: True if it's locked to C{tid}, False otherwise.
        @rtype: L{bool}
        """
	try:
	    return tid in self.__slock[ id ]
	except KeyError:
	    return False
    # is_slocked_tid()


    def sync_is_slocked_just_tid( self, tid, id ):
        """Check if data C{id} is shared locked to just C{tid} and no other
        transaction.

        @param tid: transaction identifier to check.
        @param id: data identifier to check.

        @return: True if it's shared locked just to C{tid}, False otherwise.
        @rtype: L{bool}
        """
	slocked = self.is_slocked_tid( tid, id )
	try:
	    n_locks = len( self.__slock[ id ] )
	except:
	    n_locks = 0

	if slocked and n_locks <= 1:
	    return True
	else:
	    return False
    # is_slocked_just_tid()


    def sync_unlock( self, tid, id ):
        """Release every lock C{tid} have over C{id}.

        @param tid: transaction identifier.
        @param id: data identifier.

        @return: True if it was unlocked properly.
        @rtype: L{bool}
        """
	r = False
	if self.is_xlocked( id ):
	    r = self.__do_xunlock( tid, id ) or r

	r = self.__do_sunlock( tid, id ) or r
	return r
    # unlock()


    def sync_unlock_all( self, tid ):
        """Release every lock C{tid} have.

        @param tid: transaction identifier.
        """
	lids = Set()
	try:
	    for id in self.__r_xlock[ tid ]:
		lids.add( id )
	except KeyError:
	    pass

	try:
	    for id in self.__r_slock[ tid ]:
		lids.add( id )
	except KeyError:
	    pass

	debug( 3, "unlock_all: %s" % lids )

	for id in lids:
	    self.unlock( tid, id )
    # unlock_all()


    def __do_xlock( self, tid, id ):
	self.__xlock[ id ] = tid

	l = self.__r_xlock.setdefault( tid, Set() )
	l.add( id )

	self.__do_slock( tid, id )
    # __do_xlock()


    def __do_slock( self, tid, id ):
	tids = self.__slock.setdefault( id, Set() )
	tids.add( tid )

	l = self.__r_slock.setdefault( tid, Set() )
	l.add( id )
    # __do_slock()


    def __do_xunlock( self, tid, id ):
	if not self.is_xlocked_tid( tid, id ):
	    return False

	del self.__xlock[ id ]

	# remove from inverse map
	l = self.__r_xlock[ tid ]
	del l[ id ]

	if not l:
	    del self.__r_xlock[ tid ]

	return True
    # __do_xunlock()


    def __do_sunlock( self, tid, id ):
	if not self.is_slocked_tid( tid, id ):
	    return False

	tids = self.__slock[ id ]

	del tids[ tid ]
	if not tids:
	    # there are no tids, remove shared lock
	    del self.__slock[ id ]

	l = self.__r_slock[ tid ]
	del l[ id ]
	if not l:
	    del self.__r_slock[ tid ]

	return True
    # __do_sunlock()


    def __str__( self ):
	s = [ "Locks:",
	      "   Shared:" ]
	for k, v in self.__slock.iteritems():
	    s.append( "      %s: %s" % ( k, v ) )

	s.append( "   Exclusive:" )
	for k, v in self.__xlock.iteritems():
	    s.append( "      %s: %s" % ( k, v ) )

	s.append( "-" * 79 )
	return "\n".join( s )
    # __str__()
# DataManager




class singleton( object ):
    """Class that returns a singleton of DataManager.

    @note: This class, when instantiated returns an instance of DataManager.
    """
    dm = DataManager()

    def __new__( self ):
	return self.dm
    # __new__()
# singleton
